// Copyright (C) Microsoft Corporation. All rights reserved.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Management.Automation.Host;
using System.Management.Automation.Runspaces;
using System.Management.Automation;

namespace Microsoft.VSPowerToys.VSCmdShell.Hosts
{

    /// <summary>
    /// Custom shell host for VSCmdShell that utilizes Microsoft PowerShell.
    /// Since PowerShell supports custom hosts as well, launching a seperate process is not required
    /// in this case.
    /// </summary>
    public class PowerShellHost : PSHost, IShellHost
    {

        #region Private Variables

        /// <summary>
        /// GUID for the current instance of PowerShell
        /// </summary>
        private Guid instanceID = Guid.NewGuid();

        /// <summary>
        /// User interface for PowerShell host
        /// </summary>
        private PSHostUserInterface hostUI;

        /// <summary>
        /// Runspace to use when executing user commands
        /// </summary>
        private Runspace shellRunspace;

        private Pipeline shellPipeline;

        private IShellUI shellUI;

        private string startingDirectory;

        #endregion
        
        #region IShellHost Members

        public string ShellName
        {
            get {
                return "Windows PowerShell";
            }
        }

        public char VisualStudioCommandEscapeCharacter
        {
            get 
            {
                return '!';
            }
        }

        public void ExecuteCommand(string command)
        {
            System.Threading.Thread t = new System.Threading.Thread(
                new System.Threading.ParameterizedThreadStart(this.ExecuteCommandHelper));
            t.Start(command);
        }

        /// <remarks>
        /// Currently only the environment variables from VS process is being read since 
        /// PowerShell does not keep environment variables.
        /// </remarks>
        public string ExpandEnvironmentVariables(string input)
        {
            return System.Environment.ExpandEnvironmentVariables(input);
        }


        /// <summary>
        /// Initializes the shell process, since PowerShell might prompt the user when initializing 
        /// we need to start the runspace on another thread.
        /// </summary>
        /// <param name="ui"></param>
        public void InitializeShellProcess(IShellUI ui, string startingDirectory)
        {
            this.shellUI = ui;
            this.hostUI = new PowerShellHostUI(ui);
            this.startingDirectory = startingDirectory;
            System.Threading.Thread t = new System.Threading.Thread(new System.Threading.ThreadStart(this.StartShellHelper));
            t.Start();
        }


        /// <summary>
        /// Gets the directory user is currently working on.
        /// </summary>
        /// <param name="currentLine">Current commandline including command prompt</param>
        /// <returns></returns>
        /// <remarks>In PowerShell, we can get the current directory by using "pwd" command</remarks>
        public string GetCurrentWorkingDirectory(string currentLine)
        {
            string pwd = null;
            using (Pipeline commandPipeline = this.shellRunspace.CreatePipeline())
            {
                try
                {
                    commandPipeline.Commands.Add("pwd");
                    Collection<PSObject> output = commandPipeline.Invoke();
                    pwd = output[0].ToString();
                }
                catch (RuntimeException)
                {
                    /// In case of error, we return null which will abort AutoComplete.
                    pwd = null;
                }
            }
            return pwd;
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            this.Dispose(true);
        }

        protected virtual void Dispose(bool disposing)
        {
            return;
        }

        #endregion

        #region PSHost overrides

        public override System.Globalization.CultureInfo CurrentCulture
        {
            get {
                return System.Threading.Thread.CurrentThread.CurrentCulture;
            }
        }

        public override System.Globalization.CultureInfo CurrentUICulture
        {
            get {
                return System.Threading.Thread.CurrentThread.CurrentUICulture;
            }
        }

        public override void EnterNestedPrompt()
        {
            throw new NotImplementedException();
        }

        public override void ExitNestedPrompt()
        {
            throw new NotImplementedException();
        }

        public override Guid InstanceId
        {
            get 
            {
                return this.instanceID;
            }
        }

        public override string Name
        {
            get {
                return "PowerShell Host for VSCmdShell";
            }
        }

        public override void NotifyBeginApplication()
        {
            return;
        }

        public override void NotifyEndApplication()
        {
            return;
        }

        /// <remarks>Since VSCmdShell only ends with Visual Studio, this method
        /// call is ignored
        /// </remarks>
        public override void SetShouldExit(int exitCode)
        {
            return;
        }

        public override PSHostUserInterface UI
        {
            get {
                return this.hostUI;
            }
        }

        public override Version Version
        {
            get 
            {
                return new Version(1, 0, 0, 0);
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Helper method to execute commands when there is no input (ie: When executing commands
        /// typed by user)
        /// </summary>
        /// <param name="cmd">Command to execute</param>
        private void ExecuteCommandHelper(object cmd)
        {
            try
            {
                this.ExecuteCommandHelper(cmd.ToString(), null);
            }
            catch (RuntimeException rte)
            {
                this.ExecuteCommandHelper("$input | out-default", rte.ErrorRecord);
            }
            finally
            {
                this.hostUI.Write(Environment.NewLine + PSHostStrings.PromptForShell);
            }
        }

        /// <summary>
        /// Helper method to execute commands with input objects
        /// </summary>
        /// <param name="cmd">Command to execute</param>
        /// <param name="input">Input to pass to command pipeline</param>
        private void ExecuteCommandHelper(string cmd, object input)
        {
            /// Ignore empty commands
            if (String.IsNullOrEmpty(cmd))
                return;

            lock (this.shellRunspace)
            {
                this.shellPipeline = this.shellRunspace.CreatePipeline();
            }

            // Create a pipeline for this execution
            try
            {
                this.shellPipeline.Commands.AddScript(cmd);
                /// Direct output and error to default output which is this implementation
                this.shellPipeline.Commands.Add("out-default");
                this.shellPipeline.Commands[0].MergeMyResults(PipelineResultTypes.Error, PipelineResultTypes.Output);
                this.shellUI.ApplicationRunning = true;
                if (input != null)
                {
                    this.shellPipeline.Invoke(new object[] { input });
                }
                else
                {
                    this.shellPipeline.Invoke();
                }
            }
            finally
            {
                lock (this.shellRunspace)
                {
                    this.shellPipeline.Dispose();
                    this.shellPipeline = null;
                }
                this.shellUI.ApplicationRunning = false;
                
            }
        }

        /// <summary>
        /// Helper method for starting the shell proces on another thread
        /// </summary>
        private void StartShellHelper()
        {
            this.shellRunspace = RunspaceFactory.CreateRunspace(this);
            this.shellRunspace.Open();
            /// Change directory if necessary
            if (!String.IsNullOrEmpty(this.startingDirectory) &&
                System.IO.Directory.Exists(this.startingDirectory))
            {
                using (Pipeline commandPipeline = this.shellRunspace.CreatePipeline())
                {
                    try
                    {
                        commandPipeline.Commands.AddScript("set-location \"" + this.startingDirectory + "\"");
                        commandPipeline.Invoke();
                    }
                    catch (RuntimeException)
                    {
                    }
                }
            }
            this.hostUI.Write(Environment.NewLine + PSHostStrings.PromptForShell);
        }
        #endregion

    }
}
